<?php

namespace App\Utils\Modbus;

use Exception;
use ModbusTcpClient\Composer\Read\ReadRegistersBuilder;
use ModbusTcpClient\Composer\Read\Register\BitReadRegisterAddress;
use ModbusTcpClient\Composer\Write\WriteRegistersBuilder;
use ModbusTcpClient\Network\BinaryStreamConnection;
use ModbusTcpClient\Network\NonBlockingClient;
use ModbusTcpClient\Packet\ModbusFunction\ReadCoilsRequest;
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;
use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesRequest;
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest;
use ModbusTcpClient\Packet\ModbusFunction\WriteSingleRegisterRequest;
use ModbusTcpClient\Packet\ResponseFactory;
use ModbusTcpClient\Packet\RtuConverter;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

class ModbusTcp
{

    protected BinaryStreamConnection $connection;
    protected string $host;
    protected int $port;

    public function __construct(string $host = '127.0.0.1', int $port = 501, $timeout = 0.5)
    {
        $this->connection = BinaryStreamConnection::getBuilder()
            ->setPort($port)
            ->setHost($host)
            ->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
            ->setWriteTimeoutSec($timeout) // timeout when writing/sending packet to the server
            ->setReadTimeoutSec(0.3) // timeout when waiting response from server
            ->build();
        $this->host = $host;
        $this->port = $port;
    }

    /**
     * 判断modbus寄存器是否在线
     * @return string
     */
    public function isOnline(): string
    {
        $connection = $this->connection;
        try {
            $connection->connect();
            $is_online = 1;
        } catch (\Exception $exception) {
            $is_online = 0;
        } finally {
            $connection->close();
        }
        return $is_online;
    }

    /**
     * 设置寄存器上的某一个值
     * @param int $address
     * @param int $value
     * @param string $get_type
     * @param int $bit
     * @param int $unitID
     * @return array|string
     */
    public function setValue(int $address, int $value, string $get_type = 'int16', int $bit = -99, int $unitID = 1): array|string
    {
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $uri = "tcp://{$this->host}:{$this->port}";
        $server = WriteRegistersBuilder::newWriteMultipleRegisters($uri, $unitID);
        switch ($get_type) {
            case "bit":
                if ($bit < 0 || $bit > 16) {
                    return "bit位有误";
                }
                if (!in_array($value, [0, 1])) {
                    return "bit值只有0,1";
                }
                //bit转换成uint16
                $arr = [
                    //采样口选择 16bit 前8是高字节 后8是低字节
                    ['start_address' => $address, 'get_type' => 'uint16', 'name' => 'name'],
                ];
                $data = $this->getModbusTcpTypeRead($arr);
                if (is_string($data)) {
                    return $data;
                }
                $data_model = $data['name'];
                $string = decbin($data_model);
                $string = str_pad($string, 16, "0", STR_PAD_LEFT);
                //目标的点位是在高位还是低位
                if ($bit >= 8) {
                    //高位
                    $new_string = substr_replace($string, $value, ($bit - 8), 1);
                } else {
                    //低位
                    $new_string = substr_replace($string, $value, ($bit + 8), 1);
                }
                $data_model_new_string = bindec($new_string);
                $arr = [
                    ['start_address' => $address, 'get_type' => "uint16", 'value' => $data_model_new_string],
                ];
                $info = $this->getModbusTcpTypeWrite($arr);
                if (is_string($info)) {
                    return $info;
                }
                return [];
            case "int16":
                $server = $server->int16($address, $value);
                break;
            case "uint16":
                $server = $server->uint16($address, $value);
                break;
            case "int32":
                $server = $server->int32($address, $value);
                break;
            case "uint32":
                $server = $server->uint32($address, $value);
                break;
            case "uint64":
                $server = $server->uint64($address, $value);
                break;
            case "float":
                $server = $server->float($address, $value);
                break;
            case "string":
                $server = $server->string($address, $value);
                break;
            default:
                return '类型有误';
        }
        $server->build();
        return [];
    }

    /**
     * 获取寄存器上的某一个值
     * @param int $address
     * @param string $name
     * @param string $get_type
     * @param int $bit
     * @param int $unitID
     * @return array|string
     */
    public function getValue(int $address, string $name, string $get_type = 'int16', int $bit = -99, int $unitID = 1): array|string
    {
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $ip = $this->host;
        $port = $this->port;
        $server = ReadRegistersBuilder::newReadHoldingRegisters('tcp://' . $ip . ":" . $port, $unitID);

        if ($get_type == "bit") {
            if ($bit < 0 || $bit > 16) {
                return "bit位有误";
            }
            $server = $server->bit($address, $bit, $name);
        } elseif ($get_type == "int16") {
            $server = $server->int16($address, $name);
        } elseif ($get_type == "uint16") {
            $server = $server->uint16($address, $name);
        } elseif ($get_type == "int32") {
            $server = $server->int32($address, $name);
        } elseif ($get_type == "uint32") {
            $server = $server->uint32($address, $name);
        } elseif ($get_type == "uint64") {
            $server = $server->uint64($address, $name);
        } elseif ($get_type == "float") {
            $server = $server->float($address, $name);
        } elseif ($get_type == "string") {
            $server = $server->string($address, $name);
        } else {
            $server = $server->byte($address, $name);
        }
        $requests = $server->build();
        $data = [];
        try {
            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $data = $response->getData();
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 获取modbus-tcp的值,根据数据类型进行获取
     * @param int $unitID 站号
     * @param array $data_array 数据集合
     * @return string |array
     */
    public function getModbusTcpTypeRead(array $data_array, int $unitID = 1): array|string
    {

        if (empty($data_array)) {
            return "数据有误";
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $keyName = "name";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return "数据中不存在键名【start_address】";
        }
        if (!array_key_exists($keyName, $data_array[0])) {
            return "数据中不存在键名【name】";
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return "数据中不存在键名【get_type】";
        }
        $uri = "tcp://{$this->host}:{$this->port}";
        $server = ReadRegistersBuilder::newReadHoldingRegisters($uri, $unitID);
        foreach ($data_array as $value) {
            $start_address = $value['start_address'];
            $get_type = $value['get_type'];
            $name = $value['name'];
            switch ($get_type) {
                case "bit":
                    if (!array_key_exists('bit', $value)) {
                        return "数据中不存在键名【bit】";
                    }
                    $bit = $value['bit'];
                    $server = $server->bit($start_address, $bit, $name);
                    break;
                case "int16":
                    $server = $server->int16($start_address, $name);
                    break;
                case "uint16":
                    $server = $server->uint16($start_address, $name);
                    break;
                case "int32":
                    $server = $server->int32($start_address, $name);
                    break;
                case "uint32":
                    $server = $server->uint32($start_address, $name);
                    break;
                case "uint64":
                    $server = $server->uint64($start_address, $name);
                    break;
                case "float":
                    $server = $server->float($start_address, $name);
                    break;
                case "string":
                    $server = $server->string($start_address, $name);
                    break;
                default:
                    $server = $server->byte($start_address, $name);
                    break;
            }
        }
        $requests = $server->build();
        try {
            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $data = $response->getData();
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 写入modbus-tcp的值,根据数据类型进行写入
     * @param int $unitID 站号
     * @param array $data_array 数据集合
     * @return string |array
     */
    public function getModbusTcpTypeWrite(array $data_array, int $unitID = 1): array|string
    {

        if (empty($data_array)) {
            return "数据有误";
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $keyValue = "value";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return "数据中不存在键名【start_address】";
        }
        if (!array_key_exists($keyValue, $data_array[0])) {
            return "数据中不存在键名【value】";
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return "数据中不存在键名【get_type】";
        }
        try {
            $uri = "tcp://{$this->host}:{$this->port}";
            $server = WriteRegistersBuilder::newWriteMultipleRegisters($uri, $unitID);
            foreach ($data_array as $value) {
                $start_address = $value['start_address'];
                $get_type = $value['get_type'];
                $_value = $value['value'];
                switch ($get_type) {
                    case "bit":
                    case "byte":
                        return "类型不存在【bit,byte】";
                    case "int16":
                        $server = $server->int16($start_address, $_value);
                        break;
                    case "uint16":
                        $server = $server->uint16($start_address, $_value);
                        break;
                    case "int32":
                        $server = $server->int32($start_address, $_value);
                        break;
                    case "uint32":
                        $server = $server->uint32($start_address, $_value);
                        break;
                    case "uint64":
                        $server = $server->uint64($start_address, $_value);
                        break;
                    case "int64":
                        $server = $server->int64($start_address, $_value);
                        break;
                    case "float":
                        $server = $server->float($start_address, $_value);
                        break;
                    case "double":
                        $server = $server->double($start_address, $_value);
                        break;
                    default:
                        $server = $server->string($start_address, $_value);
                        break;
                }
            }
            $requests = $server->build();

            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $data = $response->getData();
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 获取开始值范围内的寄存器的值
     * @param int $startAddress
     * @param int $number
     * @param int $unitID
     * @return array|string
     */
    public function getAddressList(int $startAddress, int $number, int $unitID = 1): array|string
    {
        try {
            $uri = "tcp://{$this->host}:{$this->port}";
            $server = ReadRegistersBuilder::newReadHoldingRegisters($uri, $unitID);
            $get_data = [];
            for ($i = $startAddress; $i <= ($startAddress + $number); $i++) {
                $server = $server->uint16($i, $i);
                $get_data[] = $i;
            }
            $data = [];
            $requests = $server->build();

            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $get_data_all = $response->getData();
                if ($get_data_all) {
                    for ($j = 0; $j < $number; $j++) {
                        $data[] = [$get_data[$j] => $get_data_all[$j]];
                    }
                }
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }


    /**
     * 获取modbus-tcp写入参数的请求和响应报文
     * @param int $startAddress
     * @param int $value
     * @param string $dataType
     * @param int $unitID
     * @return array|string
     */
    public function getModbusWriteMessage(int $startAddress, int $value, string $dataType, int $unitID = 1): array|string
    {
        $endianess = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $error = "";
        $v = "";
        switch ($dataType) {
            case 'uint8':
                if ($value < 0 || $value > 255) {
                    $error = "valid range of uint8 is 0 to 255";
                } else {
                    $v = Types::toByte($value);
                    if ($endianess & Endian::BIG_ENDIAN) {
                        $v = "\x00" . $v;
                    } else {
                        $v = $v . "\x00";
                    }
                }
                break;
            case 'int8':
                if ($value < -128 || $value > 127) {
                    $error = "valid range of int8 is -128 to 127";
                } else {
                    $v = Types::toByte($value);
                }
                if ($endianess & Endian::BIG_ENDIAN) {
                    $v = "\x00" . $v;
                } else {
                    $v = $v . "\x00";
                }
                break;
            case 'uint16':
                if ($value < 0 || $value > 65535) {
                    $error = "valid range of uint16 is 0 to 65535";
                } else {
                    $v = Types::toUint16($value);
                }
                break;
            case 'int16':
                if ($value < -32768 || $value > 32767) {
                    $error = "valid range of uint16 is -32768 to 32767";
                } else {
                    $v = Types::toInt16($value);
                }
                break;
            case 'uint32':
                if ($value < 0 || $value > 4294967295) {
                    $error = "valid range of uint32 is 0 to 4294967295";
                } else {
                    $v = Types::toUint32($value);
                }
                break;
            case 'int32':
                if ($value < -2147483648 || $value > 2147483647) {
                    $error = "valid range of int32 is -2147483648 to 2147483647";
                } else {
                    $v = Types::toInt32($value);
                }
                break;
            case 'float32':
                if ($value < -3.4e+38 || $value > 3.4e+38) {
                    $error = "valid range of float32 is -3.4e+38 to 3.4e+38";
                } else {
                    $v = Types::toReal($value);
                }
                break;
            case 'uint64':
                $value = (int)$value;
                if ($value < 0 || $value > 9223372036854775807) {
                    $error = "valid range of uint64 is 0 to 9223372036854775807";
                } else {
                    $v = Types::toUint64($value);
                }
                break;
            case 'int64':
                if ($value < 0 || $value > 9223372036854775807) {
                    $error = "valid range of int64 is -9223372036854775808 to 9223372036854775807";
                } else {
                    $v = Types::toInt64($value);
                }
                break;
            case 'float64':
                $value = (double)$value;
                if ($value < 1.7E-308 || $value > 1.7E+308) {
                    $error = "valid range of float64 is 1.7E-308 to 1.7E+308";
                } else {
                    $v = Types::toDouble($value);
                }
                break;
            default:
                $error = "invalid data type";
                break;
        }
        if ($error) {
            return $error;
        }
        try {
            $packet = new WriteMultipleRegistersRequest($startAddress, [$v], $unitID);
            $binaryData = $this->connection->connect()->sendAndReceive($packet);
            $message['response_message'] = $packet->toHex();
            $message['request_message'] = unpack('H*', $binaryData)[1];
            return $message;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 获取modbus-tcp读取参数的请求和响应报文
     * @param int $startAddress 开始地址
     * @param string $dataType 数据类型
     * @param int $quantity 数量
     * @param int $unitID
     * @return array|string
     */
    public function getModbusReadMessage(int $startAddress, string $dataType = "uint16", int $quantity = 1, int $unitID = 1): array|string
    {
        if ($quantity < 1 || $quantity > 124) {
            return "quantity值超出范围,只能在1-124之间";
        }
        try {
            $result = [];
            $packet = new ReadHoldingRegistersRequest($startAddress, $quantity, $unitID);
            $binaryData = $this->connection->connect()->sendAndReceive($packet);
            $result['response_message'] = $packet->toHex();
            $result['request_message'] = unpack('H*', $binaryData)[1];
            $response = ResponseFactory::parseResponseOrThrow($binaryData);
            $response = $response->withStartAddress($startAddress);
            foreach ($response as $address => $word) {
                $doubleWord = isset($response[$address + 1]) ? $response->getDoubleWordAt($address) : null;
                $quadWord = null;
                $value = match ($dataType) {
                    'uint16' => $word->getUInt16(),
                    'int16' => $word->getInt16(),
                    default => '-',
                };
                if (isset($response[$address + 3])) {
                    $quadWord = $response->getQuadWordAt($address);
                    $value = match ($dataType) {
                        'uint16' => $word->getUInt16(),
                        'int16' => $word->getInt16(),
                        'uint32' => $doubleWord->getUInt32(),
                        'int32' => $doubleWord->getInt32(),
                        'float32' => $doubleWord->getFloat(),
                        'uint64' => $quadWord->getUInt64(),
                        'int64' => $quadWord->getInt64(),
                        default => '-',
                    };
                }
                $highByteAsInt = $word->getHighByteAsInt();
                $lowByteAsInt = $word->getLowByteAsInt();
                $result[$address] = [
                    'highByte' => '0x' . str_pad(dechex($highByteAsInt), 2, '0', STR_PAD_LEFT),
                    'lowByte' => '0x' . str_pad(dechex($lowByteAsInt), 2, '0', STR_PAD_LEFT),
                    'highByteBits' => sprintf('%08d', decbin($highByteAsInt)),
                    'lowByteBits' => sprintf('%08d', decbin($lowByteAsInt)),
                    'value' => $value,
                ];
            }
            return $result;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * modbus-tcp FC6数据读取 --Write Single Register(写入单个寄存器)
     * @param int $startAddress 地址
     * @param int $value 值
     * @param int $unitID
     * @return array|string
     */
    public function getWriteFC6(int $startAddress, int $value, int $unitID = 1): array|string
    {
        $packet = new WriteSingleRegisterRequest($startAddress, $value, $unitID);
        try {
            $binaryData = $this->connection->connect()
                ->sendAndReceive($packet);
            $response = ResponseFactory::parseResponseOrThrow($binaryData);
            $result['response_message'] =$packet->toHex();
            $result['request_message'] = $response->toHex();
            $result['address'] = $startAddress;
            $result['value'] = $value;
            return $result;
        }catch (\Exception $exception){
            return $exception->getMessage();
        }
    }

    /**
     * 请求的地址参数编程请求报文
     * @param int $address
     * @param int $value
     * @param int $unitID
     * @return array
     */
    public function setRequestMessage(int $address,int $value, int $unitID = 1): array
    {
        $packet = new WriteSingleRegisterRequest($address, $value, $unitID);
        return ['request_message'=>$packet->toHex()];
    }

    /**
     * 读取
     * @param int $address
     * @param int $bit
     * @param int $unitID
     * @return array|string
     */
    public function getBitReadValue(int $address=0,int $bit=0, int $unitID = 1): array|string
    {
        if ($bit < 0 || $bit > 16) {
            return "bit位有误";
        }
        $uri = "tcp://{$this->host}:{$this->port}";
        try {
            $server = ReadRegistersBuilder::newReadHoldingRegisters($uri, $unitID);
            $requests = $server->bit($address, $bit,'value')->build();
            $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
            $response = $client->sendRequests($requests);
            return $response->getData();
        }catch (\Exception $exception){
            return $exception->getMessage();
        }
    }

    /**
     * 读取
     * @param int $address
     * @param bool $bit
     * @param int $unitID
     * @return array|string
     */
    public function getByteReadValue(int $address=0,bool $bit=true, int $unitID = 1): array|string
    {
        $uri = "tcp://{$this->host}:{$this->port}";
        try {
            $server = ReadRegistersBuilder::newReadHoldingRegisters($uri, $unitID);
            $requests = $server->byte($address, $bit,'value')->build();
            $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
            $response = $client->sendRequests($requests);
            return $response->getData();
        }catch (\Exception $exception){
            return $exception->getMessage();
        }
    }

    /**
     * 设置bit位的值
     * @param int $address
     * @param int $bit
     * @param int $value
     * @param int $unitID
     * @return array|string
     */
    public function setBitReadValue(int $address=0,int $bit=0,int $value=0, int $unitID = 1): array|string
    {
        if ($bit < 0 || $bit > 16) {
            return "bit位有误";
        }
        if (!in_array($value, [0, 1])) {
            return "bit值只有0,1";
        }
        $data_value = $this->getAddressList($address,1,$unitID);
        $old_value = $data_value[0][$address];
        if ($bit==0){
            $pos = -1;
        }else{
            $pos = "-".($bit+1);
        }
        try {
            $string = decbin($old_value);
            $string = str_pad($string, 16, "0", STR_PAD_LEFT);
            $new_value = bindec(substr_replace($string,$value,$pos,1));
            return $this->getModbusWriteMessage($address, $new_value, "uint16",$unitID);
        }catch (\Exception $exception){
            return $exception->getMessage();
        }
    }

}